/**
 * Copyright © 2011 Creativor Studio.
 * About license information, please see LICENSE.txt.
 */
package org.creativor.rayson.server;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.concurrent.atomic.AtomicLong;
import org.creativor.rayson.common.ClientSession;
import org.creativor.rayson.common.Invocation;
import org.creativor.rayson.common.InvocationResultType;
import org.creativor.rayson.common.RpcCallException;
import org.creativor.rayson.common.Stream;
import org.creativor.rayson.exception.ReadInvocationException;
import org.creativor.rayson.transport.common.ConnectionProtocol;
import org.creativor.rayson.transport.common.Packet;

// TODO: Auto-generated Javadoc
/**
 * A object that presents one RPC call in RPC server.
 * 
 * @author Nick Zhang
 */
public final class ServerCall
{
	
	private static final int BUFFER_SIZE = 1024;
	private static final AtomicLong UID = new AtomicLong(0);
	private static final byte[] CLIENT_RESERVED_DATA =
			new byte[ConnectionProtocol.SESSION_RESERVED_DATA_LENGTH];
	private static final int TRY_WRITE_RESULT_TIME = 3;
	private ClientSession clientSession;
	
	/**
	 * De-serialize an server call out of an request packet ,client version and
	 * remote client address.
	 * 
	 * @param remoteAddress
	 *            The address of the remote client that send this call.
	 * @param clientVersion
	 *            The version of the remote client.
	 * @param requestPacket
	 *            The request packet which remote client send.
	 * @return An server call object.
	 * @throws IOException
	 *             If can not get call data from request packet.
	 */
	public static ServerCall fromPacket(InetSocketAddress remoteAddress, byte clientVersion,
			Packet requestPacket) throws IOException
	{
		ByteArrayInputStream byteArrayInputStream =
				new ByteArrayInputStream(requestPacket.getData());
		DataInputStream dataInputStream = new DataInputStream(byteArrayInputStream);
		ServerCall serverCall = new ServerCall();
		long clientCallId;
		clientCallId = dataInputStream.readLong();
		serverCall.clientCallId = clientCallId;
		try
		{
			ClientSession clientSession = new ClientSession();
			clientSession.read(dataInputStream);
			dataInputStream.read(CLIENT_RESERVED_DATA);
			// Session Session = new RpcSessionImpl(clientSession,
			// remoteAddress);
			serverCall.clientSession =
					ClientSession.fromPortableSession(clientSession, remoteAddress.getAddress(),
							clientVersion);
			Invocation invocation = new Invocation();
			invocation.read(dataInputStream);
			serverCall.invocation = invocation;
		} catch (Throwable e)
		{
			serverCall.setException(new RpcCallException(new ReadInvocationException(
					"Read call invocation error: " + e.toString())));
		}
		return serverCall;
	}
	
	private long clientCallId;
	private RpcCallException exception;
	private long id;
	private Invocation invocation;
	private Packet responsePacket;
	private Object result;
	private long creationTime;
	
	private ServerCall()
	{
		this.id = UID.getAndIncrement();
		this.creationTime = System.currentTimeMillis();
	}
	
	/**
	 * Exception catched.
	 * 
	 * @return true, if successful
	 */
	boolean exceptionCatched()
	{
		return (exception != null);
	}
	
	/**
	 * Gets the client id.
	 * 
	 * @return the client id
	 */
	public long getClientId()
	{
		return clientCallId;
	}
	
	/**
	 * Gets the id.
	 * 
	 * @return the id
	 */
	public long getId()
	{
		return id;
	}
	
	/**
	 * Gets the invocation.
	 * 
	 * @return the invocation
	 */
	Invocation getInvocation()
	{
		return invocation;
	}
	
	/**
	 * Get response packet of this call.
	 */
	public Packet getResponsePacket()
	{
		if (responsePacket == null)
		{
			responsePacket = toResponsePacket();
		}
		return responsePacket;
	}
	
	/**
	 * Gets the client session.
	 * 
	 * @return the client session
	 */
	public ClientSession getClientSession()
	{
		return clientSession;
	}
	
	/**
	 * Sets the exception.
	 * 
	 * @param exception
	 *            the new exception
	 */
	void setException(RpcCallException exception)
	{
		this.exception = exception;
	}
	
	/**
	 * Sets the result.
	 * 
	 * @param result
	 *            the new result
	 */
	void setResult(Object result)
	{
		this.result = result;
	}
	
	private Packet toResponsePacket()
	{
		return toResponsePacket(1);
	}
	
	private Packet toResponsePacket(int tryTime)
	{
		
		Packet packet = null;
		ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(BUFFER_SIZE);
		DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream);
		try
		{
			dataOutputStream.writeLong(clientCallId);
		} catch (IOException e)
		{
			throw new RuntimeException("Can not write client call id to data output stream", e);
		}
		try
		{
			
			if (exception != null)
			{
				dataOutputStream.writeByte(InvocationResultType.EXCEPTION.getType());
				exception.write(dataOutputStream);
			} else
			{
				dataOutputStream.writeByte(InvocationResultType.SUCCESSFUL.getType());
				Stream.writePortable(dataOutputStream, result);
			}
			packet = new Packet();
		} catch (Exception e)
		{
			if (tryTime > TRY_WRITE_RESULT_TIME)
				throw new RuntimeException("Try to write call result " + tryTime
						+ " times, but still failed!");
			// return a call exception packet
			exception =
					new RpcCallException(new ReadInvocationException("Write call result error:"
							+ e.getMessage()));
			return toResponsePacket(++tryTime);
		}
		return packet;
	}
	
	/**
	 * To string.
	 * 
	 * @return the string
	 */
	@Override
	public String toString()
	{
		StringBuffer sb = new StringBuffer();
		sb.append("{");
		sb.append("id: ");
		sb.append(id);
		sb.append(", session: ");
		sb.append(this.clientSession.toString());
		sb.append(", invocation: ");
		sb.append(this.invocation.toString());
		sb.append("}");
		return sb.toString();
	}
	
	/**
	 * Return creation time.
	 */
	public long getCreationTime()
	{
		return creationTime;
	}
}
